/**
 * 
 */
package com.novartis.nims.trialsubmitter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.zip.GZIPOutputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.novartis.nims.util.ResourceAccess;
import com.novartis.nims.util.SHA1Hash;
import com.novartis.nims.util.SHA1HashException;

/**
 * @author Josh Snyder
 *
 */
class OdmSink extends TrialSubmitterStage {
	
		private Trial mTrial = null;
		private URI mInputBaseLocation = null;
		private URI mOutputLocation = null;
		private String mSubmissionNumber = null;
		private String mCroName = null;
		private String mExtensionNumber = null;

		OdmSink(TrialSubmitter ts, URI outputLocation, String submissionNumber, String croName, String extensionNumber) {
			super(ts);
			mOutputLocation = outputLocation;
			mSubmissionNumber = submissionNumber;
			mCroName = croName;
			mExtensionNumber = extensionNumber;
		}
		
		void setTrial(Trial trial) {
			mTrial = trial;
			mInputBaseLocation = mTrial.getBaseLocation();
		}
		
		void execute() throws OdmSinkException {
			this.setMessage("Generating ODM...");
			this.setProgress(0);
			GZIPOutputStream zip = null;
			FileOutputStream fis = null;
			try {
				File file = new File(mOutputLocation.getPath() + File.separator + "odm.xml.gz");
				try {
					fis = new FileOutputStream(file);
					zip = new GZIPOutputStream(fis);
				} catch (IOException e) {
					throw new OdmSinkException("Error opening ODM file for writing and zipping. Try making sure output directory is open for writing.", e);
				}
				
				InputStream xslStream = ResourceAccess.getResourceAsStream(OdmSink.class, "ImageFileTree2ODM.xslt");
				StreamSource xslSource = new StreamSource(xslStream);
				DOMSource domSource;
				Document doc = this.toXMLDocument();
				if (doc == null) {
					return;
				}
				domSource = new DOMSource(doc);
				StreamResult streamResult = new StreamResult(zip);
				TransformerFactory tf = TransformerFactory.newInstance();
				Transformer t = tf.newTransformer(xslSource);
				t.transform(domSource, streamResult);
			} catch (TransformerException e) {
				throw new OdmSinkException("Could not generate XML for ODM. This is probably a deployment error.", e);
			} finally {
				try {
					if (zip != null) zip.close();
					if (fis != null) fis.close();
				} catch (IOException e) {}
			}
		}
		


		/**
		 * Construct and return an XML representation of the trial submission.
		 *
		 * @return the XML document. null if it could not be generated.
		 * @throws OdmSinkException
		 */
		private Document toXMLDocument() throws OdmSinkException {
			DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder;
			try {
				docBuilder = dbFactory.newDocumentBuilder();
			} catch (ParserConfigurationException e) {
				return null;
			}
			Document xmlDocument = docBuilder.newDocument();
			
			Element submissionNode = xmlDocument.createElement("submission");
			DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
			String dateTime = dateFormat.format(new Date());
			submissionNode.setAttribute("croid", mCroName);
			submissionNode.setAttribute("fileidnum", mSubmissionNumber);
			submissionNode.setAttribute("asofdatetime", dateTime);
			submissionNode.setAttribute("creationdatetime", dateTime);
			xmlDocument.appendChild(submissionNode);

			//TODO if map is empty, fail
			int imagesProcessed = 0;
			int totalImages = mTrial.size();
			for (Integer imageKey : mTrial.keySet()) {
				if (this.isCancelled()) {
					return null;
				}
				SubmissionKey imageIdentifier = mTrial.getImageIdentifier(imageKey);
				URI imageReference = mTrial.getImageReference(imageKey);
				addImageToDocument(xmlDocument, imageReference, imageIdentifier);
				
				// ensure progress never runs past 100
				setProgress((int) Math.min(100, 100 * ++imagesProcessed / totalImages));
			}
			return xmlDocument;
		}
		
		/**
		 * add a new submission key to the xml document. a key corresponds to a unique
		 * element in the submission. the key is flat, but the document has a hierarchy.
		 * this method steps through the hierarchy starting from the top. at each level,
		 * it looks for an existing node matching the corresponding portion of the key.
		 * if found, it continues on the next lower level with the next portion of the
		 * key. if not found, it creates the node and all subsequent children, and
		 * identifies them with the key.
		 * 
		 * @param doc - the xml document to add the key to
		 * @param imageIdentifier - the key to add
		 * @throws OdmSinkException 
		 * @throws SHA1HashException 
		 * @throws FileNotFoundException 
		 */
		private void addImageToDocument(Document doc, URI imageReference, SubmissionKey imageIdentifier) throws OdmSinkException {
			Element submissionNode = doc.getDocumentElement();
			Element trialNode = null;
			Element subjectNode = null;
			Element visitNode = null;
			Element scanNode = null;
			Element fileNode = null;
			

			boolean foundParent = true;
			
			if (foundParent) trialNode = findDocumentElement(submissionNode, imageIdentifier.getTrialKey());
			if (!foundParent || trialNode == null) {
				foundParent = false;
				trialNode = newDocumentElement(doc, "Study", imageIdentifier.getTrialKey());
				submissionNode.appendChild(trialNode);
			}

			if (foundParent) subjectNode = findDocumentElement(trialNode, imageIdentifier.getSubjectKey());
			if (!foundParent || subjectNode == null) {
				subjectNode = newDocumentElement(doc, "Subject", imageIdentifier.getSubjectKey());
				trialNode.appendChild(subjectNode);
			}

			if (foundParent) visitNode = findDocumentElement(subjectNode, imageIdentifier.getVisitKey(), imageIdentifier.getVisitRepeatKey());
			if (!foundParent || visitNode == null) {
				visitNode = newDocumentElement(doc, "Visit", imageIdentifier.getVisitKey(), imageIdentifier.getVisitRepeatKey());
				visitNode.setAttribute("visitNumber", imageIdentifier.getVisitNumber());
				visitNode.setAttribute("extension", mExtensionNumber);
				subjectNode.appendChild(visitNode);
			}

			if (foundParent) scanNode = findDocumentElement(visitNode, imageIdentifier.getScanKey(), imageIdentifier.getScanRepeatKey());
			if (!foundParent || scanNode == null) {
				scanNode = newDocumentElement(doc, "Scan", imageIdentifier.getScanKey(), imageIdentifier.getScanRepeatKey());
				visitNode.appendChild(scanNode);
			}

			//each file node corresponds to a uniquely keyed image, so we don't have to search for an
			//existing one. also file node follows a different format than other nodes so we don't use
			//standard addDocumentElement method.
			//TODO standardize
			fileNode = doc.createElement("ImageFile");

			URI absoluteLocation = mInputBaseLocation.resolve(imageReference);
			
			FileInputStream hashStream = null;
			String hash;
			try {
				hashStream = new FileInputStream(new File(absoluteLocation));
				hash = SHA1Hash.hash(hashStream);
			} catch (FileNotFoundException e) {
				throw new OdmSinkException("Couldn't read input image file hashing. Make sure source directory is still accessible.", e);
			} catch (SHA1HashException e) {
				throw new OdmSinkException("Couldn't find SHA1 hash algorithm. This is probably a packaging or deployment error.", e);
			} finally {
				try { if (hashStream != null) hashStream.close(); } catch (IOException e) {}
			}
			
			fileNode.setAttribute("name", imageReference.getPath());
			fileNode.setAttribute("sha1hash", hash);
			scanNode.appendChild(fileNode);
		}
		
		private Element newDocumentElement(Document doc, String name, String key, String repeatKey) {
			Element newNode = doc.createElement(name);
			newNode.setAttribute("id", key);
			if (repeatKey != null) {
				newNode.setAttribute("repeatkey", repeatKey);
			}
			
			return newNode;
		}
		
		private Element newDocumentElement(Document doc, String name, String key) {
			return newDocumentElement(doc, name, key, null);
		}
		
		private Element findDocumentElement(Node context, String key) {
			return findDocumentElement(context, key, null);
		}
		
		private Element findDocumentElement(Node context, String key, String repeatKey) {

			Element child = null;
			
			NodeList children = context.getChildNodes();
			for (int i = 0; (i < children.getLength()) && (child == null); i++) {
				Element currentElement = (Element) children.item(i);
				String attrValue = currentElement.getAttribute("id");
				if (attrValue.equals(key)) {
					if (repeatKey == null) {
						child = currentElement;
					} else {
						attrValue = currentElement.getAttribute("repeatkey");
						if (attrValue.equals(repeatKey)) child = currentElement;
					}
				}
			}
			
			return child;
		}
}
